package org.liang.common.property;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * 属性文件操作工具.
 * 
 * <pre>
 * 默认读取 user.dir 路径下的配置文件，
 * 如果该文件不存在则去读取 class path 路径下的文件,
 * 如果class path 下也没有 则去所提供的路径下查找
 * </pre>
 * 
 * <pre>
 * 提供读取字符、 数组、数字、集合 等多种配置文件读取行为
 * </pre>
 * 
 * @author L.Yang
 * @version 1.0, 2015年7月15日
 */
public class PropertiesParser {

    /** 默认配置文件. */
    private static final String DEF_FILE = "application.properties";

    /** 配置文件后缀名. */
    private static final String FILE_SUFFIX = ".properties";

    private Properties props;

    /**
     * 带参的构造方法.
     * 
     * <pre>
     * 默认加载 application.properties
     * </pre>
     * 
     * @throws Exception
     *             文件未找到异常
     */
    public PropertiesParser() {
        initialize(DEF_FILE, null);
    }

    /**
     * 带参的构造方法.
     * 
     * <pre>
     * 默认读取 user.dir 路径下的配置文件，
     * 如果该文件不存在则去读取 class path 路径下的文件。
     * </pre>
     * 
     * @param fileName
     *            要读取的配置文件名 ，如果文件名为空则默认加载 application.properties
     * @throws Exception
     *             文件未找到异常
     */
    public PropertiesParser(String fileName) {
        // 1 如果文件名为空则加载默认配置文件
        // 2 如果名称中包含.properties 后缀在不添加后缀，反之则添加
        fileName = StringUtils.isEmpty(fileName) ? DEF_FILE // 1
                        : (fileName.toLowerCase().contains(FILE_SUFFIX) // 2
                                        ? fileName
                                        : fileName + FILE_SUFFIX);
        initialize(fileName, null);
    }

    /**
     * 带参的构造方法.
     * <p>
     * By default a properties file named "fileName" is loaded from the 'current working directory'.
     * If that fails, then the file located (as a resource) in the class path is loaded. If that
     * fails, then the file located (as a resource) in the package path (directory) is loaded.
     * </p>
     * 
     * @throws Exception
     *             文件未找到异常
     */
    public PropertiesParser(String fileName, String directory) {
        initialize(fileName, directory);
    }


    /**
     * 初始化方法.
     * <p>
     * By default a properties file named "fileName" is loaded from the 'current working directory'.
     * If that fails, then the file located (as a resource) in the class path is loaded. If that
     * fails, then the file located (as a resource) in the package path (directory) is loaded.
     * </p>
     * 
     * @throws Exception
     *             文件未找到异常
     */
    private void initialize(String fileName, String directory){
        InputStream in = null;
        try {
            // 1 如果文件名为空则加载默认配置文件
            // 2 如果名称中包含.properties 后缀 在不添加后缀，反之则添加
            fileName = StringUtils.isEmpty(fileName) ? DEF_FILE // 1
                            : (fileName.toLowerCase().contains(FILE_SUFFIX) // 2
                                            ? fileName
                                            : fileName + FILE_SUFFIX);
            props = new Properties();
            if (new File(fileName).exists()) { // 在 current working directory 中找
                try {
                    in = new BufferedInputStream(new FileInputStream(fileName));
                    props.load(in);
                } catch (IOException ioe) {
                    String error = "Properties file: '" + fileName + "' could not be read.";
                    throw new RuntimeException(error, ioe);
                }
            } else { // 去 class path 或 package path 中找
                try {
                    in = getRescource(fileName, directory);
                } catch (Exception e) {
                    // 永远不会发生的异常，在class目录必定放置配置文件
                }
                if (in == null) throw new RuntimeException(fileName + "not found in class path");
                try {
                    props.load(in);
                } catch (IOException ioe) {
                    String error = "Resource properties file: '" + directory + fileName + "' "
                                   + "could not be read from the classpath.";
                    throw new RuntimeException(error, ioe);
                }
            }
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * 获取 资源文件 .
     * 
     * <pre>
     * 先去class 路径下寻找，如果没有，则去包路径（directory）下寻找.
     * </pre>
     * 
     * @param fileName
     *            文件名称
     * @param directory
     *            文件目录
     * @return 输入流实例
     * @throws Exception
     *             文件未找到异常
     */
    private InputStream getRescource(String fileName, String directory)
                    throws Exception {
        InputStream in;
        ClassLoader cl = getClass().getClassLoader();
        if (cl == null) cl = findClassloader();
        if (cl == null)
            throw new Exception("Unable to find a class loader on the current thread or class.");
        in = cl.getResourceAsStream(fileName);
        if (in == null) {
            in = cl.getResourceAsStream("/" + fileName);
        }

        if (in == null && directory != null) {
            in = cl.getResourceAsStream(directory + fileName);
        }
        return in;
    }

    /**
     * 获取class loader.
     */
    private ClassLoader findClassloader() {
        // work-around set context loader for windows-service started jvms (QUARTZ-748)
        if (Thread.currentThread().getContextClassLoader() == null
            && getClass().getClassLoader() != null) {
            Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
        }
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * Get the trimmed String value of the property with the given <code>name</code>. If the value
     * the empty String (after trimming), then it returns null.
     * 
     * @param name
     *            属性名
     */
    public String getString(String name) {
        return getString(name, null);
    }

    /**
     * Get the trimmed String value of the property with the given <code>name</code> or the given
     * default value if the value is null or empty after trimming.
     */
    public String getString(String name, String def) {
        String val = props.getProperty(name, def);
        if (val == null) return def;
        val = val.trim();
        return (val.length() == 0) ? def : val;
    }

    /**
     * 获取 数组 .
     * 
     * <pre>
     * 属性分隔符: ','。
     * </pre>
     * 
     * @param name
     *            属性名称
     * @return 属性下数据数组
     */
    public String[] getStringArray(String name) {
        return getStringArray(name, null);
    }

    /**
     * 获取 数组.
     * 
     * <pre>
     *  属性分隔符: ',', 如果为空则将默认值设定到返回数组中。
     * </pre>
     * 
     * @param name
     *            属性名称
     * @param def
     *            默认值
     * @return 属性下数据数组
     */
    public String[] getStringArray(String name, String[] def) {
        String vals = getString(name);
        if (vals == null) return def;
        StringTokenizer stok = new StringTokenizer(vals, ",");
        ArrayList<String> strs = new ArrayList<String>();
        try {
            while (stok.hasMoreTokens()) {
                strs.add(stok.nextToken().trim());
            }
            return (String[]) strs.toArray(new String[strs.size()]);
        } catch (Exception e) {
            return def;
        }
    }

    /**
     * 获取boolean类型数据.
     * 
     * @param name
     *            属性值
     * @return 数据值
     */
    public boolean getBoolean(String name) {
        return getBoolean(name, false);
    }

    /**
     * 获取boolean类型数据.
     * 
     * <pre>
     * 如果为空则，设定为默认值。
     * </pre>
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     */
    public boolean getBoolean(String name, boolean def) {
        String val = getString(name);
        return (val == null) ? def : Boolean.valueOf(val).booleanValue();
    }

    /**
     * 获取double类型数据.
     * 
     * @param name
     *            属性名
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public double getDouble(String name)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) throw new NumberFormatException(" null string");
        try {
            return Double.parseDouble(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 获取double类型数据.
     * 
     * <pre>
     * 如果数据值为空，则设定为默认值。
     * </pre>
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public double getDouble(String name, double def)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) {
            return def;
        }

        try {
            return Double.parseDouble(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 获取 double 数组
     * 
     * <pre>
     * 属性分隔符: ',' .
     * </pre>
     * 
     * @param name
     *            属性名
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public double[] getDoubleArray(String name)
                    throws NumberFormatException {
        return getDoubleArray(name, null);
    }
    
    /**
     * 获取 double 数组
     * 
     * <pre>
     * 属性分隔符: ',' .
     * </pre>
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public double[] getDoubleArray(String name, double[] def)
                    throws NumberFormatException {
        String vals = getString(name);
        if (vals == null) {
            return def;
        }

        StringTokenizer stok = new StringTokenizer(vals, ",");
        ArrayList<Double> ints = new ArrayList<Double>();
        try {
            while (stok.hasMoreTokens()) {
                try {
                    ints.add(Double.valueOf(stok.nextToken().trim()));
                } catch (NumberFormatException nfe) {
                    throw new NumberFormatException(" '" + vals + "'");
                }
            }

            double[] outInts = new double[ints.size()];
            for (int i = 0; i < ints.size(); i++) {
                outInts[i] = ((Double) ints.get(i)).doubleValue();
            }
            return outInts;
        } catch (Exception e) {
            return def;
        }
    }

    
    /**
     * 根据属性名获取Long型数据.
     * 
     * <pre>
     * 添加方法详细说明:实现逻辑、算法信息，并举例。
     * </pre>
     * 
     * @param name
     *            属性名
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public long getLong(String name)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) {
            throw new NumberFormatException(" null string");
        }

        try {
            return Long.parseLong(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 获取 Long型 数据.
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public long getLong(String name, long def)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) {
            return def;
        }

        try {
            return Long.parseLong(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 根据属性名获取int型数据.
     * 
     * @param name
     *            属性名
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public int getInt(String name)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) {
            throw new NumberFormatException(" null string");
        }

        try {
            return Integer.parseInt(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 获取 int 数据.
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public int getInt(String name, int def)
                    throws NumberFormatException {
        String val = getString(name);
        if (val == null) {
            return def;
        }

        try {
            return Integer.parseInt(val);
        } catch (NumberFormatException nfe) {
            throw new NumberFormatException(" '" + val + "'");
        }
    }

    /**
     * 获取 int 数组
     * 
     * <pre>
     * 属性分隔符: ',' .
     * </pre>
     * 
     * @param name
     *            属性名
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public int[] getIntArray(String name)
                    throws NumberFormatException {
        return getIntArray(name, null);
    }

    /**
     * 获取 int 数组
     * 
     * <pre>
     * 属性分隔符: ',' .
     * </pre>
     * 
     * @param name
     *            属性名
     * @param def
     *            默认值
     * @return 数据值
     * @throws NumberFormatException
     *             当为空时，抛出数据格式化异常。
     */
    public int[] getIntArray(String name, int[] def)
                    throws NumberFormatException {
        String vals = getString(name);
        if (vals == null) {
            return def;
        }

        StringTokenizer stok = new StringTokenizer(vals, ",");
        ArrayList<Integer> ints = new ArrayList<Integer>();
        try {
            while (stok.hasMoreTokens()) {
                try {
                    ints.add(Integer.valueOf(stok.nextToken().trim()));
                } catch (NumberFormatException nfe) {
                    throw new NumberFormatException(" '" + vals + "'");
                }
            }

            int[] outInts = new int[ints.size()];
            for (int i = 0; i < ints.size(); i++) {
                outInts[i] = ((Integer) ints.get(i)).intValue();
            }
            return outInts;
        } catch (Exception e) {
            return def;
        }
    }


    /**
     * 根据前缀获取属性值数组.
     * 
     * <pre>
     * 通过属性前缀 获取属性值
     * </pre>
     * 
     * @param prefix
     *            前缀
     * @return 值数组
     */
    public String[] getPropertyGroups(String prefix) {
        Enumeration<?> keys = props.propertyNames();
        HashSet<String> groups = new HashSet<String>(10);

        if (!prefix.endsWith(".")) {
            prefix += ".";
        }

        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            if (key.startsWith(prefix)) {
                String groupName = key.substring(prefix.length(),
                                key.indexOf('.', prefix.length()));
                groups.add(groupName);
            }
        }

        return (String[]) groups.toArray(new String[groups.size()]);
    }

    /**
     * Get all properties that start with the given prefix.
     * 
     * @param prefix
     *            The prefix for which to search. If it does not end in a "." then one will be added
     *            to it for search purposes.
     * @return Group of <code>Properties</code> that start with the given prefix.
     */
    public Map<String, String> getPropertyGroup(String prefix) {
        return getPropertyGroup(prefix, false, null);
    }


    /**
     * Get all properties that start with the given prefix.
     * 
     * @param prefix
     *            The prefix for which to search. If it does not end in a "." then one will be added
     *            to it for search purposes.
     * @param stripPrefix
     *            Whether to strip off the given <code>prefix</code> in the result's keys.
     * @return Group of <code>Properties</code> that start with the given prefix, optionally have
     *         that prefix removed.
     */
    public Map<String, String> getPropertyGroup(String prefix, boolean stripPrefix) {
        return getPropertyGroup(prefix, stripPrefix, null);
    }


    /**
     * Get all properties that start with the given prefix.
     * 
     * @param prefix
     *            The prefix for which to search. If it does not end in a "." then one will be added
     *            to it for search purposes.
     * @param stripPrefix
     *            Whether to strip off the given <code>prefix</code> in the result's keys.
     * @param excludedPrefixes
     *            Optional array of fully qualified prefixes to exclude. For example if
     *            <code>prefix</code> is "a.b.c", then <code>excludedPrefixes</code> might be
     *            "a.b.c.ignore".
     * @return Group of <code>Properties</code> that start with the given prefix, optionally have
     *         that prefix removed, and do not include properties that start with one of the given
     *         excluded prefixes.
     */
    public Map<String, String> getPropertyGroup(String prefix, boolean stripPrefix,
                                                String[] excludedPrefixes) {
        Enumeration<?> keys = props.propertyNames();
        Map<String, String> group = new LinkedHashMap<String, String>();

        prefix = !prefix.endsWith(".") ? prefix += "." : prefix;

        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            if (key.startsWith(prefix)) {

                boolean exclude = false;
                if (excludedPrefixes != null) {
                    for (int i = 0; (i < excludedPrefixes.length) && (exclude == false); i++) {
                        exclude = key.startsWith(excludedPrefixes[i]);
                    }
                }

                if (exclude == false) {
                    String value = getString(key, "");

                    if (stripPrefix) {
                        group.put(key.substring(prefix.length()), value);
                    } else {
                        group.put(key, value);
                    }
                }
            }
        }
        return group;
    }
}
